/*
 * Copyright 2016-2023 The OSHI Project Contributors
 * SPDX-License-Identifier: MIT
 */
package oshi.hardware.platform.windows;

import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Objects;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.sun.jna.platform.win32.COM.COMException;
import com.sun.jna.platform.win32.COM.WbemcliUtil.WmiResult;

import oshi.annotation.concurrent.ThreadSafe;
import oshi.driver.windows.wmi.*;
import oshi.driver.windows.wmi.MSAcpiThermalZoneTemperature.TemperatureProperty;
import oshi.driver.windows.wmi.OhmHardware.IdentifierProperty;
import oshi.driver.windows.wmi.OhmSensor.ValueProperty;
import oshi.driver.windows.wmi.Win32Fan.SpeedProperty;
import oshi.driver.windows.wmi.Win32Processor.VoltProperty;
import oshi.hardware.SensorFanInfo;
import oshi.hardware.GpuInfo;
import oshi.hardware.common.AbstractSensors;
import oshi.util.CollectionUtil;
import oshi.util.platform.windows.WmiQueryHandler;
import oshi.util.platform.windows.WmiUtil;

/**
 * Sensors from WMI or Open Hardware Monitor
 */
@ThreadSafe
final class WindowsSensors extends AbstractSensors {

    private static final Logger LOG = LoggerFactory.getLogger(WindowsSensors.class);

    private static final String COM_EXCEPTION_MSG = "COM exception: {}";

    @Override
    public double queryCpuTemperature() {
        // attempt to fetch value from Libre Hardware Monitor
        double tempC = getTempFromLHM();
        if (tempC > 0d) {
            return tempC;
        }

        // Attempt to fetch value from Open Hardware Monitor if it is running,
        // as it will give the most accurate results and the time to query (or
        // attempt) is trivial
        tempC = getTempFromOHM();
        if (tempC > 0d) {
            return tempC;
        }

        // If we get this far, OHM is not running. Try from WMI
        tempC = getTempFromWMI();

        // Other fallbacks to WMI are unreliable so we omit them
        // Win32_TemperatureProbe is the official location but is not currently
        // populated and is "reserved for future use"
        return tempC;
    }

    private static double getTempFromLHM() {
        WmiQueryHandler h = Objects.requireNonNull(WmiQueryHandler.createInstance());
        boolean comInit = false;
        try {
            comInit = h.initCOM();
            WmiResult lhmHardware = LhmHardware.queryHwIdentifier(h, "Hardware", "CPU");
            if (lhmHardware.getResultCount() > 0) {
                LOG.debug("Found Temperature data in Libre Hardware Monitor");
                String cpuIdentifier = WmiUtil.getString(lhmHardware, LhmHardware.IdentifierProperty.IDENTIFIER, 0);
                if (cpuIdentifier.length() > 0) {
                    WmiResult lhmSensors = LhmSensor.querySensorValue(h, cpuIdentifier, "Temperature");
                    if (lhmSensors.getResultCount() > 0) {
                        double sum = 0;
                        for (int i = 0; i < lhmSensors.getResultCount(); i++) {
                            sum += WmiUtil.getFloat(lhmSensors, LhmSensor.ValueProperty.VALUE, i);
                        }
                        return sum / lhmSensors.getResultCount();
                    }
                }
            }
        } catch (COMException e) {
            LOG.warn(COM_EXCEPTION_MSG, e.getMessage());
        } finally {
            if (comInit) {
                h.unInitCOM();
            }
        }
        return 0;
    }


    private static double getTempFromOHM() {
        WmiQueryHandler h = Objects.requireNonNull(WmiQueryHandler.createInstance());
        boolean comInit = false;
        try {
            comInit = h.initCOM();
            WmiResult ohmHardware = OhmHardware.queryHwIdentifier(h, "Hardware", "CPU");
            if (ohmHardware.getResultCount() > 0) {
                LOG.debug("Found Temperature data in Open Hardware Monitor");
                String cpuIdentifier = WmiUtil.getString(ohmHardware, IdentifierProperty.IDENTIFIER, 0);
                if (cpuIdentifier.length() > 0) {
                    WmiResult ohmSensors = OhmSensor.querySensorValue(h, cpuIdentifier, "Temperature");
                    if (ohmSensors.getResultCount() > 0) {
                        double sum = 0;
                        for (int i = 0; i < ohmSensors.getResultCount(); i++) {
                            sum += WmiUtil.getFloat(ohmSensors, ValueProperty.VALUE, i);
                        }
                        return sum / ohmSensors.getResultCount();
                    }
                }
            }
        } catch (COMException e) {
            LOG.warn(COM_EXCEPTION_MSG, e.getMessage());
        } finally {
            if (comInit) {
                h.unInitCOM();
            }
        }
        return 0;
    }

    private static double getTempFromWMI() {
        double tempC = 0d;
        long tempK = 0L;
        WmiResult<TemperatureProperty> result = MSAcpiThermalZoneTemperature.queryCurrentTemperature();
        if (result.getResultCount() > 0) {
            LOG.debug("Found Temperature data in WMI");
            tempK = WmiUtil.getUint32asLong(result, TemperatureProperty.CURRENTTEMPERATURE, 0);
        }
        if (tempK > 2732L) {
            tempC = tempK / 10d - 273.15;
        } else if (tempK > 274L) {
            tempC = tempK - 273d;
        }
        return tempC < 0d ? 0d : tempC;
    }

    @Override
    public int[] queryFanSpeeds() {

        int[] fanSpeeds = getFansFromLHM();
        if (fanSpeeds.length > 0) {
            return fanSpeeds;
        }
        // Attempt to fetch value from Open Hardware Monitor if it is running
        fanSpeeds = getFansFromOHM();
        if (fanSpeeds.length > 0) {
            return fanSpeeds;
        }

        // If we get this far, OHM is not running.
        // Try to get from conventional WMI
        fanSpeeds = getFansFromWMI();
        if (fanSpeeds.length > 0) {
            return fanSpeeds;
        }

        // Default
        return new int[0];
    }

    private static int[] getFansFromLHM() {
        WmiQueryHandler h = Objects.requireNonNull(WmiQueryHandler.createInstance());
        boolean comInit = false;
        try {
            comInit = h.initCOM();
            // 查询主板
            WmiResult lhmHardware = LhmHardware.queryHwIdentifier(h, "Hardware", "Motherboard");
            if (lhmHardware.getResultCount() > 0) {
                String motherBoardIdentifier = WmiUtil.getString(lhmHardware, LhmHardware.IdentifierProperty.IDENTIFIER, 0);
                // 查询SuperIO
                WmiResult superIo = LhmHardware.queryHwIdentifierByParent(h, "Hardware", "SuperIO",
                    motherBoardIdentifier);
                if (superIo.getResultCount() > 0) {
                    LOG.debug("Found Fan data in Libre Hardware Monitor");
                    String superIoIdentifier = WmiUtil.getString(superIo, LhmHardware.IdentifierProperty.IDENTIFIER, 0);
                    if (superIoIdentifier.length() > 0) {
                        WmiResult lhmSensors = LhmSensor.querySensorValue(h, superIoIdentifier, "Fan");
                        if (lhmSensors.getResultCount() > 0) {
                            int[] fanSpeeds = new int[lhmSensors.getResultCount()];
                            for (int i = 0; i < lhmSensors.getResultCount(); i++) {
                                fanSpeeds[i] = (int) WmiUtil.getFloat(lhmSensors, LhmSensor.ValueProperty.VALUE, i);
                            }
                            return fanSpeeds;
                        }
                    }
                }

            }
        } catch (COMException e) {
            LOG.warn(COM_EXCEPTION_MSG, e.getMessage());
        } finally {
            if (comInit) {
                h.unInitCOM();
            }
        }
        return new int[0];
    }


    private static int[] getFansFromOHM() {
        WmiQueryHandler h = Objects.requireNonNull(WmiQueryHandler.createInstance());
        boolean comInit = false;
        try {
            comInit = h.initCOM();
            WmiResult<IdentifierProperty> ohmHardware = OhmHardware.queryHwIdentifier(h, "Hardware", "CPU");
            if (ohmHardware.getResultCount() > 0) {
                LOG.debug("Found Fan data in Open Hardware Monitor");
                String cpuIdentifier = WmiUtil.getString(ohmHardware, IdentifierProperty.IDENTIFIER, 0);
                if (cpuIdentifier.length() > 0) {
                    WmiResult<ValueProperty> ohmSensors = OhmSensor.querySensorValue(h, cpuIdentifier, "Fan");
                    if (ohmSensors.getResultCount() > 0) {
                        int[] fanSpeeds = new int[ohmSensors.getResultCount()];
                        for (int i = 0; i < ohmSensors.getResultCount(); i++) {
                            fanSpeeds[i] = (int) WmiUtil.getFloat(ohmSensors, ValueProperty.VALUE, i);
                        }
                        return fanSpeeds;
                    }
                }
            }
        } catch (COMException e) {
            LOG.warn(COM_EXCEPTION_MSG, e.getMessage());
        } finally {
            if (comInit) {
                h.unInitCOM();
            }
        }
        return new int[0];
    }

    private static int[] getFansFromWMI() {
        WmiResult<SpeedProperty> fan = Win32Fan.querySpeed();
        if (fan.getResultCount() > 1) {
            LOG.debug("Found Fan data in WMI");
            int[] fanSpeeds = new int[fan.getResultCount()];
            for (int i = 0; i < fan.getResultCount(); i++) {
                fanSpeeds[i] = (int) WmiUtil.getUint64(fan, SpeedProperty.DESIREDSPEED, i);
            }
            return fanSpeeds;
        }
        return new int[0];
    }

    @Override
    public double queryCpuVoltage() {
        // Attempt to fetch value from Open Hardware Monitor if it is running
        double volts = getVoltsFromOHM();
        if (volts > 0d) {
            return volts;
        }

        // If we get this far, OHM is not running.
        // Try to get from conventional WMI
        volts = getVoltsFromWMI();

        return volts;
    }

    private static double getVoltsFromOHM() {
        WmiQueryHandler h = Objects.requireNonNull(WmiQueryHandler.createInstance());
        boolean comInit = false;
        try {
            comInit = h.initCOM();
            WmiResult<IdentifierProperty> ohmHardware = OhmHardware.queryHwIdentifier(h, "Sensor", "Voltage");
            if (ohmHardware.getResultCount() > 0) {
                LOG.debug("Found Voltage data in Open Hardware Monitor");
                // Look for identifier containing "cpu"
                String cpuIdentifier = null;
                for (int i = 0; i < ohmHardware.getResultCount(); i++) {
                    String id = WmiUtil.getString(ohmHardware, IdentifierProperty.IDENTIFIER, i);
                    if (id.toLowerCase(Locale.ROOT).contains("cpu")) {
                        cpuIdentifier = id;
                        break;
                    }
                }
                // If none found, just get the first one
                if (cpuIdentifier == null) {
                    cpuIdentifier = WmiUtil.getString(ohmHardware, IdentifierProperty.IDENTIFIER, 0);
                }
                // Now fetch sensor
                WmiResult<ValueProperty> ohmSensors = OhmSensor.querySensorValue(h, cpuIdentifier, "Voltage");
                if (ohmSensors.getResultCount() > 0) {
                    return WmiUtil.getFloat(ohmSensors, ValueProperty.VALUE, 0);
                }
            }
        } catch (COMException e) {
            LOG.warn(COM_EXCEPTION_MSG, e.getMessage());
        } finally {
            if (comInit) {
                h.unInitCOM();
            }
        }
        return 0d;
    }

    private static double getVoltsFromWMI() {
        WmiResult<VoltProperty> voltage = Win32Processor.queryVoltage();
        if (voltage.getResultCount() > 1) {
            LOG.debug("Found Voltage data in WMI");
            int decivolts = WmiUtil.getUint16(voltage, VoltProperty.CURRENTVOLTAGE, 0);
            // If the eighth bit is set, bits 0-6 contain the voltage
            // multiplied by 10. If the eighth bit is not set, then the bit
            // setting in VoltageCaps represents the voltage value.
            if (decivolts > 0) {
                if ((decivolts & 0x80) == 0) {
                    decivolts = WmiUtil.getUint32(voltage, VoltProperty.VOLTAGECAPS, 0);
                    // This value is really a bit setting, not decivolts
                    if ((decivolts & 0x1) > 0) {
                        return 5.0;
                    } else if ((decivolts & 0x2) > 0) {
                        return 3.3;
                    } else if ((decivolts & 0x4) > 0) {
                        return 2.9;
                    }
                } else {
                    // Value from bits 0-6, divided by 10
                    return (decivolts & 0x7F) / 10d;
                }
            }
        }
        return 0d;
    }


    @Override
    protected List<SensorFanInfo> queryMotherboardFans() {
        List<SensorFanInfo> fans = getMotherboardFansFromLHM();
        if (CollectionUtil.isNotEmpty(fans)) {
            return fans;
        }
        return new ArrayList<>();
    }

    private static List<SensorFanInfo> getMotherboardFansFromLHM() {
        WmiQueryHandler h = Objects.requireNonNull(WmiQueryHandler.createInstance());
        boolean comInit = false;
        try {
            comInit = h.initCOM();
            // 查询主板
            WmiResult lhmHardware = LhmHardware.queryHwIdentifier(h, "Hardware", "Motherboard");
            if (lhmHardware.getResultCount() > 0) {
                String motherBoardIdentifier = WmiUtil.getString(lhmHardware, LhmHardware.IdentifierProperty.IDENTIFIER, 0);
                // 查询SuperIO
                WmiResult superIo = LhmHardware.queryHwIdentifierByParent(h, "Hardware", "SuperIO",
                    motherBoardIdentifier);
                if (superIo.getResultCount() > 0) {
                    LOG.debug("Found Fan data in Libre Hardware Monitor");
                    String superIoIdentifier = WmiUtil.getString(superIo, LhmHardware.IdentifierProperty.IDENTIFIER, 0);
                    if (superIoIdentifier.length() > 0) {
                        WmiResult lhmSensors = LhmSensor.querySensorInfo(h, superIoIdentifier, "Fan");
                        if (lhmSensors.getResultCount() > 0) {
                            List<SensorFanInfo> fans = new ArrayList<>();
                            for (int i = 0; i < lhmSensors.getResultCount(); i++) {
                                String name = WmiUtil.getString(lhmSensors, LhmSensor.SensorProperty.NAME, i);
                                float value = WmiUtil.getFloat(lhmSensors, LhmSensor.SensorProperty.VALUE, i);
                                float min = WmiUtil.getFloat(lhmSensors, LhmSensor.SensorProperty.MIN, i);
                                float max = WmiUtil.getFloat(lhmSensors, LhmSensor.SensorProperty.MAX, i);
                                SensorFanInfo fan = new SensorFanInfo();
                                fan.setName(name);
                                fan.setValue(value);
                                fan.setMin(min);
                                fan.setMax(max);
                                fans.add(fan);
                            }
                            return fans;
                        }
                    }
                }

            }
        } catch (COMException e) {
            LOG.warn(COM_EXCEPTION_MSG, e.getMessage());
        } finally {
            if (comInit) {
                h.unInitCOM();
            }
        }
        return new ArrayList<>();
    }


    @Override
    protected List<SensorFanInfo> queryGpuFans() {
        List<SensorFanInfo> fans = getGpuFansInfoFromLHM();
        if (CollectionUtil.isNotEmpty(fans)) {
            return fans;
        }
        return new ArrayList<>();
    }

    private static List<SensorFanInfo> getGpuFansInfoFromLHM() {
//        WmiQueryHandler h = Objects.requireNonNull(WmiQueryHandler.createInstance());
//        boolean comInit = false;
//        try {
//            comInit = h.initCOM();
//            // 查询主板
//            WmiResult lhmHardware = LhmHardware.queryHwIdentifier(h, "Hardware", "Motherboard");
//            if (lhmHardware.getResultCount() > 0) {
//                String motherBoardIdentifier = WmiUtil.getString(lhmHardware, LhmHardware.IdentifierProperty.IDENTIFIER, 0);
//                // 查询SuperIO
//                WmiResult superIo = LhmHardware.queryHwIdentifierByParent(h, "Hardware", "SuperIO",
//                    motherBoardIdentifier);
//                if (superIo.getResultCount() > 0) {
//                    LOG.debug("Found Fan data in Libre Hardware Monitor");
//                    String superIoIdentifier = WmiUtil.getString(superIo, LhmHardware.IdentifierProperty.IDENTIFIER, 0);
//                    if (superIoIdentifier.length() > 0) {
//                        WmiResult lhmSensors = LhmSensor.querySensorInfo(h, superIoIdentifier, "Fan");
//                        if (lhmSensors.getResultCount() > 0) {
//                            List<SensorFanInfo> fans= new ArrayList<>();
//                            for (int i = 0; i < lhmSensors.getResultCount(); i++) {
//                                String name = WmiUtil.getString(lhmSensors, LhmSensor.InfoProperty.NAME, i);
//                                float value = WmiUtil.getFloat(lhmSensors, LhmSensor.InfoProperty.VALUE, i);
//                                float min = WmiUtil.getFloat(lhmSensors, LhmSensor.InfoProperty.MIN, i);
//                                float max = WmiUtil.getFloat(lhmSensors, LhmSensor.InfoProperty.MAX, i);
//                                SensorFanInfo fan = new SensorFanInfo();
//                                fan.setName(name);
//                                fan.setValue(value);
//                                fan.setMin(min);
//                                fan.setMax(max);
//                                fans.add(fan);
//                            }
//                            return fans;
//                        }
//                    }
//                }
//
//            }
//        } catch (COMException e) {
//            LOG.warn(COM_EXCEPTION_MSG, e.getMessage());
//        } finally {
//            if (comInit) {
//                h.unInitCOM();
//            }
//        }
        return new ArrayList<>();
    }

    @Override
    protected List<GpuInfo> queryGpuInfos() {
        List<GpuInfo> infos = getGpuInfosFromLHM();
        if (CollectionUtil.isNotEmpty(infos)) {
            return infos;
        }
        return new ArrayList<>();
    }

    private static List<GpuInfo> getGpuInfosFromLHM() {
        List<GpuInfo> gpuInfos = new ArrayList<>();
        WmiQueryHandler h = Objects.requireNonNull(WmiQueryHandler.createInstance());
        boolean comInit = false;
        try {
            comInit = h.initCOM();
            // 查询GPU
            WmiResult lhmHardware = LhmHardware.queryHwPropertyUseLike(h, "Hardware", "Gpu");
            if (lhmHardware.getResultCount() > 0) {
                for (int i = 0; i < lhmHardware.getResultCount(); i++) {
                    GpuInfo gpuInfo = new GpuInfo();
                    String gpuIdentifier = WmiUtil.getString(lhmHardware, LhmHardware.HardwareProperty.IDENTIFIER, i);
                    String name = WmiUtil.getString(lhmHardware, LhmHardware.HardwareProperty.NAME, i);
                    // 负载
                    double load = 0.0;
                    // 温度
                    double temperature = 0.0;
                    // 风扇
                    double fanSpeed = 0.0;
                    // gpu总内存
                    double ram = 0.0;
                    // gpu使用内存
                    double ramUsed = 0.0;
                    // gpu剩余内存
                    double ramFree = 0.0;

                    if (gpuIdentifier.length() > 0) {
                        // 获取负载
                        WmiResult lhmLoadSensors = LhmSensor.querySensorInfo(h, gpuIdentifier, "Load");
                        if (lhmLoadSensors.getResultCount() > 0) {
                            for (int j = 0; j < lhmLoadSensors.getResultCount(); j++) {
                                String sensortName = WmiUtil.getString(lhmLoadSensors, LhmSensor.SensorProperty.NAME, j);
                                if ("GPU CORE".equalsIgnoreCase(sensortName)) {
                                    load = WmiUtil.getFloat(lhmLoadSensors, LhmSensor.SensorProperty.VALUE, j);
                                    break;
                                }
                            }
                        }
                        // 获取温度
                        WmiResult lhmTempSensors = LhmSensor.querySensorInfo(h, gpuIdentifier, "Temperature");
                        if (lhmTempSensors.getResultCount() > 0) {
                            for (int j = 0; j < lhmTempSensors.getResultCount(); j++) {
                                float value = WmiUtil.getFloat(lhmTempSensors, LhmSensor.SensorProperty.VALUE, j);
                                temperature += value;
                            }
                            temperature = temperature / lhmTempSensors.getResultCount();
                        }

                        // 获取风扇
                        WmiResult lhmFanSensors = LhmSensor.querySensorInfo(h, gpuIdentifier, "Fan");
                        if (lhmFanSensors.getResultCount() > 0) {
                            for (int j = 0; j < lhmFanSensors.getResultCount(); j++) {
                                float value = WmiUtil.getFloat(lhmFanSensors, LhmSensor.SensorProperty.VALUE, j);
                                fanSpeed += value;
                            }
                            fanSpeed = fanSpeed / lhmFanSensors.getResultCount();
                        }
                        // 获取内存
                        WmiResult lhmMemorySensors = LhmSensor.querySensorInfo(h, gpuIdentifier, "SmallData");
                        if (lhmMemorySensors.getResultCount() > 0) {
                            for (int j = 0; j < lhmMemorySensors.getResultCount(); j++) {
                                String memorySensorName = WmiUtil.getString(lhmMemorySensors, LhmSensor.SensorProperty.NAME, j);
                                if ("GPU Memory Total".equalsIgnoreCase(memorySensorName)) {
                                    float value = WmiUtil.getFloat(lhmMemorySensors, LhmSensor.SensorProperty.VALUE, j);
                                    ram = value;
                                } else if ("GPU Memory Used".equalsIgnoreCase(memorySensorName)) {
                                    float value = WmiUtil.getFloat(lhmMemorySensors, LhmSensor.SensorProperty.VALUE, j);
                                    ramUsed = value;
                                } else if ("GPU Memory Free".equalsIgnoreCase(memorySensorName)) {
                                    float value = WmiUtil.getFloat(lhmMemorySensors, LhmSensor.SensorProperty.VALUE, j);
                                    ramFree = value;
                                }
                            }
                        }
                    }
                    gpuInfo.setName(name);
                    gpuInfo.setLoad(load);
                    gpuInfo.setTemperature(temperature);
                    gpuInfo.setFanspeed(fanSpeed);
                    gpuInfo.setMemoryTotal(ram);
                    gpuInfo.setMemoryUsed(ramUsed);
                    gpuInfo.setMemoryFree(ramFree);
                    gpuInfos.add(gpuInfo);
                }
            }
        } catch (COMException e) {
            LOG.warn(COM_EXCEPTION_MSG, e.getMessage());
        } finally {
            if (comInit) {
                h.unInitCOM();
            }
        }
        return gpuInfos;
    }

}
